/*
 * CCVisu is a tool for visual graph clustering
 * and general force-directed graph layout.
 * This file is part of CCVisu.
 *
 * Copyright (C) 2005-2011  Dirk Beyer
 *
 * CCVisu is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * CCVisu is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with CCVisu; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please find the GNU Lesser General Public License in file
 * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt
 *
 * Dirk Beyer    (firstname.lastname@uni-passau.de)
 * University of Passau, Bavaria, Germany
 */
package org.sosy_lab.ccvisu;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.sosy_lab.ccvisu.clustering.ClustererMinDistPerc;
import org.sosy_lab.ccvisu.clustering.interfaces.Clusterer;
import org.sosy_lab.ccvisu.graph.GraphData;
import org.sosy_lab.ccvisu.graph.GraphVertex;
import org.sosy_lab.ccvisu.graph.NameVisibility;
import org.sosy_lab.ccvisu.graph.Position;
import org.sosy_lab.ccvisu.layout.Minimizer;
import org.sosy_lab.ccvisu.layout.MinimizerBarnesHut;
import org.sosy_lab.ccvisu.layout.MinimizerOptions;
import org.sosy_lab.ccvisu.measuring.ClusterQuality;
import org.sosy_lab.ccvisu.measuring.calldep.DependencyCalculator;
import org.sosy_lab.ccvisu.measuring.calldep.DependencyOptions;
import org.sosy_lab.ccvisu.readers.InputFormat;
import org.sosy_lab.ccvisu.readers.ReaderData;
import org.sosy_lab.ccvisu.readers.ReaderDataGraph;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphAUX;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphCVS;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphDOX;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphFilter;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphODS;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphRSF;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphSVN;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphSoftware;
import org.sosy_lab.ccvisu.readers.ReaderDataGroup;
import org.sosy_lab.ccvisu.readers.ReaderDataLayoutLAY;
import org.sosy_lab.ccvisu.readers.filter.Filter;
import org.sosy_lab.ccvisu.readers.filter.Filters;
import org.sosy_lab.ccvisu.readers.filter.WhitelistFilter;
import org.sosy_lab.ccvisu.writers.OutputFormat;
import org.sosy_lab.ccvisu.writers.Writer;
import org.sosy_lab.ccvisu.writers.WriterDataCalldep;
import org.sosy_lab.ccvisu.writers.WriterDataClustersRSF;
import org.sosy_lab.ccvisu.writers.WriterDataGraphGML;
import org.sosy_lab.ccvisu.writers.WriterDataGraphGXL;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutLAY;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutSVG;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutVRML;
import org.sosy_lab.ccvisu.writers.WriterDataRSF;
import org.sosy_lab.ccvisu.writers.WriterDataSTATS;
import org.sosy_lab.ccvisu.writers.WriterGui;
import org.sosy_lab.ccvisu.writers.WriterNull;
import org.sosy_lab.common.LogManager;
import org.sosy_lab.common.configuration.Configuration;
import org.sosy_lab.common.configuration.IntegerOption;
import org.sosy_lab.common.configuration.InvalidConfigurationException;
import org.sosy_lab.common.configuration.Option;
import org.sosy_lab.common.configuration.Options;

@Options
public class CCVisuController {

  /** End of line.*/
  public final static String   endl                        = System.getProperty("line.separator");

  @Option(name="input.format",
      description="The format of the input file.")
  private InputFormat          inputFormat                 = InputFormat.RSF;

  @Option(name="input.name",
      description="The file or stream (e.g. stdin) to read input data from.")
  private String               inputName                   = "stdin";

  @Option(name="output.format",
      description="The format of the output file.")
  private OutputFormat         outputFormat                = OutputFormat.DISP;

  @Option(name="output.name",
      description="The file or stream (e.g. stdout) to write output data to.")
  private String               outputName                  = "stdout";

  @Option(name="layout.dimension",
      description="The number of dimensions of the layout, up to 3.",
      values={ "2", "3" })
  @IntegerOption(min=2, max=3)
  private int                  dimension                   = 2;

  @Option(name="writer.showAllLabels",
      description="Annotate each vertex with its name.")
  protected boolean            showAllLabels               = false;

  @Option(name="layout.initLayout",
      description="A file in format LAY. Its layout is used as initial layout.")
  private String               initialLayoutFile           = "";

  @Option(name="layout.initLayout.fixed",
      description="Fix positions for vertices from initial layout given by "
          + "option 'layout.initLayout'.")
  private boolean              fixedInitPos                = false;

  @Option(name="output.fontSize",
      description="The font size of vertex annotations.")
  @IntegerOption(min=0)
  private int                  fontSize                    = 14;

  @Option(name="clustering.numberOfClusters",
      description="The maximum number of clusters to extract.")
  @IntegerOption(min=0)
  private int                  numberOfClusters            = 0;

  // TODO: transform this into a file option
  @Option(name="clustering.writeClusteringMeasures",
      description="Calculate the measures of the given clustering (group file)"
          + " and write it to the specified file.")
  private String               writeClusteringMeasures     = "";

  @Option(name="clustering.clusterMergeDistancePercent",
      description="Merge partitions within distance (percent of layout diagonal).")
  private float                clusterMergeDistancePercent = 0.05f;

  @Option(name="clustering.clusterMinDistancePercent",
      description="Stop clustering on min. distance (percent of layout diagonal).")
  private float                clusterMinDistancePercent   = 0.4f;

  @Option(name="dependency.enable",
      description="Calculate and visualize dependencies.")
  private boolean              cdep                        = false;

  @Option(name="initGroups",
      description="Assign vertices to groups (and colors) according to file <str>.")
  private String               initGroupsFileName          = "";

  private GraphData            initialLayout               = null;

  private final Configuration  config;

  private final LogManager     logger;

  private Collection<Filter>   filters;

  public CCVisuController(Configuration config, LogManager logger)
      throws InvalidConfigurationException {

    config.inject(this);

    this.config = config;
    this.logger = logger;

    filters = new Filters(config);
  }

  public int getDimension() {
    return dimension;
  }

  public void setDimension(int dimension) {
    this.dimension = dimension;
  }

  public int getFontSize() {
    return fontSize;
  }

  public void setFontSize(int fontSize) {
    this.fontSize = fontSize;
  }

  public void setInputFormat(InputFormat inputFormat) {
    this.inputFormat = inputFormat;
  }

  public String getInputName() {
    return inputName;
  }

  public void setInputName(String inputName) {
    this.inputName = inputName;
  }

  public String getInitialLayout() {
    return initialLayoutFile;
  }

  public void setInitialLayout(String text) {
    this.initialLayoutFile = text;

  }

  public boolean getIsShowAllLabels() {
    return showAllLabels;
  }

  public OutputFormat getOutputFormat() {
    return outputFormat;
  }

  /**
   * Processing the actual task. Performs the following steps.
   * 1) Creates the appropriate input reader and reads the input.
   * 2) Computes the layout (if necessary).
   * 3) Creates the appropriate output writer and writes the output.
   * @param options  Command-line options, already parsed.
   * @param config
   * @throws InterruptedException
   */
  public void process(CCVisuOptions options, Configuration config)
      throws IOException, InterruptedException, InvalidConfigurationException {

    assert(options.getGraph() != null);
    GraphData graph = options.getGraph();

    processVisual(graph);

    createAndRunMinimizer(graph);
    runDepAnalysis(graph);
    runClusteringAndWriteResults(graph, initGroupsFileName);
    writeOutput(outputName, outputFormat, options);
  }

  private void createAndRunMinimizer(GraphData graph) {
    // Determine if we need to compute a layout.
    if ((inputFormat != InputFormat.LAY || outputFormat == OutputFormat.LAY) // only use minimizer if layout transformation is requested
      && outputFormat != OutputFormat.RSF // RSF output does not care about layout
      && outputFormat != OutputFormat.GML
      && outputFormat != OutputFormat.GXL) {

      // Set minimizer algorithm.
      // So far there is only one implemented in CCVisu.
      try {
        MinimizerOptions minimizerOptions = new MinimizerOptions(config);
        Minimizer minimizer = new MinimizerBarnesHut(minimizerOptions,
            graph, config, logger);
        minimizer.minimizeEnergy();
      } catch (InvalidConfigurationException e) {
        e.printStackTrace();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  private void runDepAnalysis(GraphData graph)
      throws InvalidConfigurationException {
    if (cdep) {
      DependencyCalculator depCalc = new DependencyCalculator(logger, graph,
          new DependencyOptions(config));
      depCalc.calculateAndSetDegree();
    }
  }

  private void runClusteringAndWriteResults(GraphData graph,
      String initGroupsFileName) throws InterruptedException {

    // Determine if we need to compute a clustering.
    if (numberOfClusters > 0 || outputFormat == OutputFormat.CRSF) {

      Clusterer clusterer = new ClustererMinDistPerc(graph, logger,
          numberOfClusters, clusterMergeDistancePercent, clusterMinDistancePercent);

      graph.clearClustering();
      clusterer.runClusteringAndUpdateGroups(graph);
    }

    // Calculate measures for clustering and append the results to the specified file.
    writeClusteringMeasures(graph, initGroupsFileName, writeClusteringMeasures);
  }

  private void writeClusteringMeasures(GraphData graph,
      String initGroupsFileName, String writeClusterMeasuresTo) {

    if ((writeClusterMeasuresTo != null) && (writeClusterMeasuresTo.length() > 0)) {
      try {
        String clusteringName = initGroupsFileName;
        if ((clusteringName == null) || (clusteringName.length() == 0)) {
          clusteringName = inputName;
        }
        if ((clusteringName == null) || (clusteringName.length() == 0)) {
          clusteringName = initialLayoutFile;
        }
        if ((clusteringName == null) || (clusteringName.length() == 0)) {
          clusteringName = "UNNAMED";
        }

        FileWriter fileWriter = new FileWriter(writeClusterMeasuresTo, true);
        fileWriter.append(String.format("%s\t%d\t%d\t%d\t%d\t%f\t%f\t%f\t%f\n",
            clusteringName,
            graph.getNumberOfClusters(),
            graph.getVertices().size(),
            graph.getEdges().size(),
            ClusterQuality.cutOfClustering(graph),
            ClusterQuality.modularizationQualityOfGraph(graph),
            ClusterQuality.edgeNormalizedCutOfClustering(graph),
            ClusterQuality.edgeNormalizedCutOfClusteringV2(graph),
            ClusterQuality.modularityOfClustering(graph)));
        fileWriter.flush();

      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  public void processVisual(GraphData graph)
      throws IOException, InvalidConfigurationException {
    assert (graph != null);

    // Check for illegal option combinations...
    if (inputFormat == InputFormat.LAY && outputFormat == OutputFormat.RSF) {
      throw new IllegalArgumentException(
        "Usage error: Cannot produce RSF output from LAY input.");
    }

    readGraph(graph);
    loadOrCalcInitialLayout();
    applyInitialLayout(graph);

    // Initialize groups.
    readInitialGroups(graph, initGroupsFileName);
  }

  private void loadOrCalcInitialLayout() throws IOException {

    if (initialLayoutFile.trim().length() > 0) {
      BufferedReader initialLayoutStream = null;
      initialLayout = new GraphData(logger);

      // Read initial (pre-computed) layout from file.
      // This sets the vertices.
      logger.log(Level.FINE, "Loading initial layout from file", initialLayoutFile);
      try {
        initialLayoutStream = new BufferedReader(new FileReader(initialLayoutFile));
        (new ReaderDataLayoutLAY(logger, initialLayoutStream)).read(initialLayout);
      } catch (IOException e) {
        throw new IOException("Exception while opening file '"
            + initialLayoutFile + "' for reading.");
      }

      logger.log(Level.FINE, "" + initialLayout.getVertices().size()
          + " vertices read. Initial layout reading finished.");

      // Close the input file.
      try {
        initialLayoutStream.close();
      } catch (Exception e) {
        logger.logUserException(Level.WARNING, e, "Exception while closing input file: ");
      }

      // Reset vertex degrees, i.e., use the degrees from the graph
      // and ignore the degree from read layout.
      for (GraphVertex vertex : initialLayout.getVertices()) {
        vertex.setDegree(0);
      }
    }
  }

  private void applyInitialLayout(GraphData graph) {
    // vertex.fixedInitPos == true means that the minimizer does not change
    //   vertex's position.

    if (fixedInitPos && initialLayout != null) {
      for (GraphVertex vertex : graph.getVertices()) {
        // If the current vertex exists in the read initial layout,
        // then mark its position as fixed.
        if (initialLayout.getVertexByName(vertex.getName()) != null) {
          vertex.setFixedPos(true);
        }
      }
    }

    if (initialLayout == graph) {
      logger.log(Level.FINEST, "The initial layout and the graph are the same.");
      return;
    }

    if ((inputFormat != InputFormat.LAY)) {
      logger.log(Level.FINEST, "Initialize the vertices with random positions.");
      // Initialize with random positions.
      for (GraphVertex vertex : graph.getVertices()) {
        Position newPosition = new Position();
        newPosition.setX(2 * (float) Math.random() - 1);

        if (dimension >= 2) {
          newPosition.setY(2 * (float) Math.random() - 1);
        } else {
          newPosition.setY(0);
        }

        if (dimension == 3) {
          newPosition.setZ(2 * (float) Math.random() - 1);
        } else {
          newPosition.setZ(0);
        }

        vertex.setPosition(newPosition);
      }
    }

    // Copy positions and properties from the initial layout
    //   that was read from file.
    copyPositionsAndProperties(graph.getVertices());
  }

  private void copyPositionsAndProperties(List<GraphVertex> vertices) {
    if (initialLayout != null) {
      for (GraphVertex vertex : vertices) {
        GraphVertex initialVertex = initialLayout.getVertexByName(vertex.getName());
        if (initialVertex != null) {
          vertex.setColor(initialVertex.getColor());
          vertex.setShowName(initialVertex);
          vertex.setPosition(new Position(initialVertex.getPosition()));
        }
      }
    }
  }

  private void readGraph(GraphData graph)
      throws IOException, InvalidConfigurationException {

    BufferedReader reader = createInputReader();
    try {
      ReaderData graphReader = getReader(reader);

      // Set filter for selection of user relations for visualization.
      if (graphReader instanceof ReaderDataGraph) {
        if (filters.size() > 0) {
          // The user has given relations to select.
          graphReader = new ReaderDataGraphFilter(logger,
              (ReaderDataGraph) graphReader, filters);

        } else {
          if (inputFormat == InputFormat.DOX && outputFormat != OutputFormat.RSF) {
            // The user has not given any relation name to select,
            //   but wants to actually draw the relation.
            // TODO: update this info (new options)
            logger.log(Level.WARNING, "Using default DOX-derived relation refFile."
                + "Change this with parameter '-relSelect <rel-name>'");

            filters.add(new WhitelistFilter("refFile"));
            graphReader = new ReaderDataGraphFilter(logger,
                (ReaderDataGraph) graphReader, filters);
          }
        }
      }

      // Read the data using the reader (i.e., fill into existing graph structure).
      graphReader.read(graph);
      logger.log(Level.FINE, graph.getVertices().size()
          + " vertices read. Graph reading finished.");

      // Handle vertex options.
      if (showAllLabels) {
        for (GraphVertex vertex : graph.getVertices()) {
          // showAllLabels (annotate each vertex with its name).
          vertex.setShowName(NameVisibility.Priority.STARTUP, true);
        }
      }

    } finally {
      reader.close();
    }
  }

  private BufferedReader createInputReader() throws IOException {
    // Setup of input reader.
    BufferedReader reader = null;
    try {
      InputStream inputStream = null;

      if (inputName.equalsIgnoreCase("stdin")) {
        inputStream = System.in;
      } else {
        inputStream = new FileInputStream(inputName);
      }

      if (inputName.endsWith(InputFormat.ODS.getFileExtension())) {
        ZipInputStream zipInputStream = new ZipInputStream(inputStream);
        // Look for file entry 'content.xml' in the ZIP file.
        ZipEntry zipEntry = zipInputStream.getNextEntry();
        while (zipEntry != null && !zipEntry.getName().equals("content.xml")) {
          zipEntry = zipInputStream.getNextEntry();
        }
        if (zipEntry == null) {
          logger.log(Level.WARNING, "I/O Error reading 'content.xml' from ODS file.");
        }
        inputStream = zipInputStream;
      }

      reader = new BufferedReader(new InputStreamReader(inputStream));
    } catch (IOException e) {
      throw new IOException("Exception while opening file '" + inputName
          + "' for reading.", e);
    }

    return reader;
  }

  private ReaderData getReader(BufferedReader reader)
      throws IOException, InvalidConfigurationException {
    switch (inputFormat) {
    case CVS:
      return new ReaderDataGraphCVS(logger, reader, config);
    case SVN:
      return new ReaderDataGraphSVN(logger, reader, inputFormat);
    case DOX:
      return new ReaderDataGraphDOX(logger, reader, inputName);
    case ODS:
      return new ReaderDataGraphODS(logger, reader, inputFormat);
    case RSF:
      return new ReaderDataGraphRSF(logger, reader);
    case SRC:
      return new ReaderDataGraphSoftware(logger, reader, config);
    case AUX:
      // Graph is set by calling client. -- Do nothing.
      return  new ReaderDataGraphAUX(logger, reader);
    case LAY:
      return new ReaderDataLayoutLAY(logger, reader);
    default:

      throw new IOException("Runtime error: Unexpected input format '"
          + inputFormat.toString() + "'.");
    }
  }

  private void readInitialGroups(GraphData graph, String initGroupsFileName) {
    if (!initGroupsFileName.equals("")) {
      try {
        ReaderData groupReader = new ReaderDataGroup(logger,
            new BufferedReader(new FileReader(initGroupsFileName)));
        groupReader.read(graph);
      } catch (FileNotFoundException e) {
        logger.logUserException(Level.WARNING, e, "Error reading file -- file not found: "
            + initGroupsFileName);
      }
    }
  }

  public void writeOutput(String outputFileName, OutputFormat outputFormat,
      CCVisuOptions options) throws IOException {

    PrintWriter printer = null;
    try {
      OutputStream outputStream = null;
      if (outputFileName.equalsIgnoreCase("stdout")) {
        outputStream = System.out;
      } else {
        outputStream = new FileOutputStream(outputFileName);
      }
      printer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream)));
    } catch (IOException e) {
      throw new IOException("Exception while opening file '" + outputFileName
          + "' for writing.", e);
    }

    try {
      // Write the data using the writer.
      Writer writer;
      try {
        writer = getWriter(outputFormat, printer, options);
      } catch (InvalidConfigurationException e) {
        logger.logUserException(Level.SEVERE, e, "Writer could not be created "
            + "due to invalid configuration.");
        printer.close();
        return;
      }
      writer.write();
      printer.flush();
    } finally {
      printer.close();
    }
  }

  private Writer getWriter(OutputFormat outputFormat, PrintWriter printer,
      CCVisuOptions options) throws IOException, InvalidConfigurationException {

    GraphData graph = options.getGraph();

    switch (outputFormat) {
    case RSF: // Graph in RSF.
      return new WriterDataRSF(printer, graph);
    case CRSF: // Clustering in RSF.
      return new WriterDataClustersRSF(printer, graph);
    case GML: // Graph in GraphML format.
      return new WriterDataGraphGML(printer, graph);
    case GXL: // Graph in GXL format.
      return new WriterDataGraphGXL(printer, graph);
    case LAY: // Layout in text format LAY.
      return new WriterDataLayoutLAY(printer, graph, options, config);
    case VRML: // Layout in VRML format.
      return new WriterDataLayoutVRML(printer, graph, options, this, config);
    case SVG: // Layout in SVG format.
      return new WriterDataLayoutSVG(printer, graph, options, this, config,
          showAllLabels);
    case STATS: // Statistics in text format LAY.
      return new WriterDataSTATS(printer, graph);
    case CALLDEP: // Nodewise information for the calldep algorithm
      return new WriterDataCalldep(printer, graph, config, logger);
    case DISP: // Graph in a Window.
      return new WriterGui(this, options, config, logger);
    case NULL: // Give no output
      return new WriterNull();
    default:
      throw new IOException("Runtime error: Unexpected output format '"
          + outputFormat.toString() + "'.");
    }
  }
}
